Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit b51d04953f58b191bfc5ab9dc78e61983694d410


Parents : d717679
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-03T18:31:45-06:00

feat(App): add emergency banner for active mode and enhance changelog modal with version tracking; improve notification logic for incoming messages

Changes
Diff

diff --git a/meshchatx/src/frontend/components/App.vue b/meshchatx/src/frontend/components/App.vue
index 6024141f..21bd4229 100644
--- a/meshchatx/src/frontend/components/App.vue
+++ b/meshchatx/src/frontend/components/App.vue
@@ -3,6 +3,17 @@
:class="{ dark: config?.theme === 'dark' }"
class="h-screen w-full flex flex-col bg-slate-50 dark:bg-zinc-950 transition-colors"
>
+ <!-- emergency banner -->
+ <div
+ v-if="appInfo?.emergency"
+ class="relative z-[100] bg-red-600 text-white px-4 py-2 text-center text-sm font-bold shadow-md animate-pulse"
+ >
+ <div class="flex items-center justify-center gap-2">
+ <MaterialDesignIcon icon-name="alert-decagram" class="size-5" />
+ <span>{{ $t("app.emergency_mode_active") }}</span>
+ </div>
+ </div>
+
<RouterView v-if="$route.name === 'auth'" />
<template v-else>
@@ -437,7 +448,7 @@
<ConfirmDialog />
<CommandPalette />
<IntegrityWarningModal />
- <ChangelogModal ref="changelogModal" />
+ <ChangelogModal ref="changelogModal" :app-version="appInfo?.version" />
<TutorialModal ref="tutorialModal" />
<!-- identity switching overlay -->
@@ -723,7 +734,8 @@ export default {
}
// show notification for new messages if window is not focussed
- if (!document.hasFocus()) {
+ // only for incoming messages
+ if (!document.hasFocus() && json.lxmf_message?.is_incoming) {
NotificationUtils.showNewMessageNotification(
json.remote_identity_name,
json.lxmf_message?.content
@@ -761,8 +773,12 @@ export default {
this.hasCheckedForModals = true;
if (this.appInfo && !this.appInfo.tutorial_seen) {
this.$refs.tutorialModal.show();
- } else if (this.appInfo && this.appInfo.changelog_seen_version !== this.appInfo.version) {
- // show changelog if version changed
+ } else if (
+ this.appInfo &&
+ this.appInfo.changelog_seen_version !== "999.999.999" &&
+ this.appInfo.changelog_seen_version !== this.appInfo.version
+ ) {
+ // show changelog if version changed and not silenced forever
this.$refs.changelogModal.show();
}
}

diff --git a/meshchatx/src/frontend/components/ChangelogModal.vue b/meshchatx/src/frontend/components/ChangelogModal.vue
index c651aca3..619abb2c 100644
--- a/meshchatx/src/frontend/components/ChangelogModal.vue
+++ b/meshchatx/src/frontend/components/ChangelogModal.vue
@@ -50,9 +50,9 @@
<div v-else-if="error" class="flex flex-col items-center justify-center h-full text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
- <v-btn color="#3b82f6" variant="flat" class="text-white font-bold" @click="fetchChangelog"
- >Retry</v-btn
- >
+ <v-btn color="blue" variant="flat" class="font-bold uppercase px-6" rounded="lg" @click="fetchChangelog">
+ Retry
+ </v-btn>
</div>
<div
@@ -66,20 +66,30 @@
<!-- Footer -->
<v-divider class="dark:border-zinc-800"></v-divider>
- <v-card-actions class="px-6 py-4 bg-gray-50 dark:bg-zinc-950/50">
- <v-checkbox
- v-model="dontShowAgain"
- :label="$t('app.do_not_show_again', 'Do not show again for this version')"
- density="compact"
- hide-details
- color="blue"
- class="my-0 text-gray-700 dark:text-zinc-300 font-medium"
- ></v-checkbox>
+ <v-card-actions class="px-6 py-4 bg-gray-50 dark:bg-zinc-950/50 flex-wrap gap-y-2">
+ <div class="flex flex-col">
+ <v-checkbox
+ v-model="dontShowAgain"
+ :label="$t('app.do_not_show_again', 'Do not show again for this version')"
+ density="compact"
+ hide-details
+ color="blue"
+ class="my-0 text-gray-700 dark:text-zinc-300 font-medium"
+ ></v-checkbox>
+ <v-checkbox
+ v-model="dontShowEver"
+ :label="$t('app.do_not_show_ever', 'Do not show ever again')"
+ density="compact"
+ hide-details
+ color="red"
+ class="my-0 text-gray-700 dark:text-zinc-300 font-medium"
+ ></v-checkbox>
+ </div>
<v-spacer></v-spacer>
<v-btn
- variant="tonal"
+ variant="flat"
color="blue"
- class="px-8 font-black tracking-tighter text-white uppercase"
+ class="px-8 font-black tracking-tighter uppercase text-white dark:text-zinc-900"
rounded="xl"
@click="close"
>
@@ -121,9 +131,9 @@
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
- <v-btn color="#3b82f6" variant="flat" class="text-white font-bold" @click="fetchChangelog"
- >Retry</v-btn
- >
+ <v-btn color="blue" variant="flat" class="font-bold uppercase px-6" rounded="lg" @click="fetchChangelog">
+ Retry
+ </v-btn>
</div>
<div v-else class="changelog-content max-w-none prose dark:prose-invert pb-20">
@@ -138,6 +148,12 @@
<script>
export default {
name: "ChangelogModal",
+ props: {
+ appVersion: {
+ type: String,
+ default: "",
+ },
+ },
data() {
return {
visible: false,
@@ -146,9 +162,13 @@ export default {
changelogHtml: "",
version: "",
dontShowAgain: false,
+ dontShowEver: false,
};
},
computed: {
+ currentVersion() {
+ return this.version || this.appVersion;
+ },
isPage() {
return this.$route?.meta?.isPage === true;
},
@@ -187,18 +207,43 @@ export default {
}
},
async close() {
+ // mark as seen for current version automatically on close if not already marked
+ if (!this.dontShowEver && !this.dontShowAgain) {
+ try {
+ await window.axios.post("/api/v1/app/changelog/seen", {
+ version: this.currentVersion || "0.0.0",
+ });
+ } catch (e) {
+ console.error("Failed to auto-mark changelog as seen:", e);
+ }
+ } else {
+ await this.markAsSeen();
+ }
this.visible = false;
- // logic moved to onVisibleUpdate
},
- async onVisibleUpdate(val) {
- if (!val && this.dontShowAgain) {
+ async markAsSeen() {
+ if (this.dontShowEver) {
try {
await window.axios.post("/api/v1/app/changelog/seen", {
- version: this.version,
+ version: "999.999.999",
});
} catch (e) {
- console.error("Failed to mark changelog as seen:", e);
+ console.error("Failed to mark changelog as seen forever:", e);
}
+ } else if (this.dontShowAgain) {
+ try {
+ await window.axios.post("/api/v1/app/changelog/seen", {
+ version: this.currentVersion,
+ });
+ } catch (e) {
+ console.error("Failed to mark changelog as seen for this version:", e);
+ }
+ }
+ },
+ async onVisibleUpdate(val) {
+ if (!val) {
+ // handle case where dialog is closed by clicking outside or ESC
+ await this.markAsSeen();
}
},
},

diff --git a/meshchatx/src/frontend/components/ColourPickerDropdown.vue b/meshchatx/src/frontend/components/ColourPickerDropdown.vue
index d1c09cfd..f3f35000 100644
--- a/meshchatx/src/frontend/components/ColourPickerDropdown.vue
+++ b/meshchatx/src/frontend/components/ColourPickerDropdown.vue
@@ -22,7 +22,7 @@
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
- <div v-if="isShowingMenu" class="absolute left-0 z-10 ml-4">
+ <div v-if="isShowingMenu" class="absolute left-0 z-[100] mt-2">
<v-color-picker
v-model="colourPickerValue"
:modes="['hex']"

diff --git a/meshchatx/src/frontend/components/LxmfUserIcon.vue b/meshchatx/src/frontend/components/LxmfUserIcon.vue
index be96bebd..32a2ac28 100644
--- a/meshchatx/src/frontend/components/LxmfUserIcon.vue
+++ b/meshchatx/src/frontend/components/LxmfUserIcon.vue
@@ -1,16 +1,25 @@
<template>
- <div v-if="customImage" class="rounded-full overflow-hidden" :class="iconClass">
+ <div
+ v-if="customImage"
+ class="rounded-full overflow-hidden shrink-0 flex items-center justify-center"
+ :class="iconClass || 'size-6'"
+ >
<img :src="customImage" class="w-full h-full object-cover" />
</div>
<div
v-else-if="iconName"
- class="p-2 rounded-full"
- :style="{ color: iconForegroundColour, 'background-color': iconBackgroundColour }"
+ class="p-[15%] rounded-full shrink-0 flex items-center justify-center"
+ :style="{ 'background-color': iconBackgroundColour }"
+ :class="iconClass || 'size-6'"
>
- <MaterialDesignIcon :icon-name="iconName" :class="iconClass" />
+ <MaterialDesignIcon :icon-name="iconName" class="w-full h-full" :style="{ color: iconForegroundColour }" />
</div>
- <div v-else class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-2 rounded-full">
- <MaterialDesignIcon icon-name="account-outline" :class="iconClass" />
+ <div
+ v-else
+ class="bg-gray-200 dark:bg-zinc-700 text-gray-500 dark:text-gray-400 p-[15%] rounded-full shrink-0 flex items-center justify-center"
+ :class="iconClass || 'size-6'"
+ >
+ <MaterialDesignIcon icon-name="account-outline" class="w-full h-full" />
</div>
</template>
@@ -32,15 +41,15 @@ export default {
},
iconForegroundColour: {
type: String,
- default: "",
+ default: "#6b7280",
},
iconBackgroundColour: {
type: String,
- default: "",
+ default: "#e5e7eb",
},
iconClass: {
type: String,
- default: "size-6",
+ default: "",
},
},
};

diff --git a/meshchatx/src/frontend/components/MaterialDesignIcon.vue b/meshchatx/src/frontend/components/MaterialDesignIcon.vue
index 7c508a5e..3d9279d3 100644
--- a/meshchatx/src/frontend/components/MaterialDesignIcon.vue
+++ b/meshchatx/src/frontend/components/MaterialDesignIcon.vue
@@ -5,6 +5,8 @@
role="img"
:aria-label="iconName"
fill="currentColor"
+ width="100%"
+ height="100%"
style="display: inline-block; vertical-align: middle; shape-rendering: inherit"
class="antialiased"
>
@@ -20,18 +22,26 @@ export default {
props: {
iconName: {
type: String,
- required: true,
+ required: false,
+ default: "",
},
},
computed: {
mdiIconName() {
+ if (!this.iconName) return "mdiAccountOutline";
+
+ // if already starts with mdi and is camelCase, return as is
+ if (this.iconName.startsWith("mdi") && /[A-Z]/.test(this.iconName)) {
+ return this.iconName;
+ }
+
// convert icon name from lxmf icon appearance to format expected by the @mdi/js library
// e.g: alien-outline -> mdiAlienOutline
- // https://pictogrammers.github.io/@mdi/font/5.4.55/
return (
"mdi" +
this.iconName
.split("-")
+ .filter(word => word.length > 0)
.map((word) => {
// capitalise first letter of each part
return word.charAt(0).toUpperCase() + word.slice(1);
@@ -40,8 +50,13 @@ export default {
);
},
iconPath() {
- // find icon, otherwise fallback to question mark, and if that doesn't exist, show nothing...
- return mdi[this.mdiIconName] || mdi["mdiProgressQuestion"] || "";
+ if (!mdi) return "";
+
+ const path = mdi[this.mdiIconName];
+ if (path) return path;
+
+ // fallback logic
+ return mdi["mdiHelpCircleOutline"] || mdi["mdiProgressQuestion"] || "";
},
},
};

diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue
index 2d15f2a8..ab8ec209 100644
--- a/meshchatx/src/frontend/components/about/AboutPage.vue
+++ b/meshchatx/src/frontend/components/about/AboutPage.vue
@@ -154,7 +154,8 @@
<div class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{{ $t("about.dependency_chain") }}
</div>
- <div class="relative">
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 relative">
+ <!-- Main Dependencies -->
<div class="flex flex-col space-y-4">
<!-- MeshChatX -->
<div class="flex items-center gap-4">
@@ -213,65 +214,65 @@
</div>
</div>
- <!-- Side Dependencies -->
- <div class="mt-8 pt-6 border-t border-gray-100 dark:border-zinc-800">
- <div
- class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
- >
- {{ $t("about.other_core_components", "Other Core Components") }}
- </div>
- <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
- <div v-if="appInfo.lxst_version" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">LXST</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ appInfo.lxst_version }}</span
- >
- </div>
- <div class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Python</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ appInfo.python_version }}</span
- >
- </div>
- <div v-if="electronVersion" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Electron</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ electronVersion }}</span
- >
- </div>
- <div v-if="chromeVersion" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Chrome</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ chromeVersion }}</span
- >
+ <!-- Side & Python Dependencies -->
+ <div class="space-y-8">
+ <!-- Side Dependencies -->
+ <div>
+ <div
+ class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
+ >
+ {{ $t("about.other_core_components", "Other Core Components") }}
</div>
- <div v-if="nodeVersion" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500 uppercase">Node</span>
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
- >v{{ nodeVersion }}</span
- >
+ <div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
+ <div v-if="appInfo.lxst_version" class="flex flex-col">
+ <span class="text-[10px] font-black text-blue-500 uppercase">LXST</span>
+ <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
+ >v{{ appInfo.lxst_version }}</span
+ >
+ </div>
+ <div class="flex flex-col">
+ <span class="text-[10px] font-black text-blue-500 uppercase">Python</span>
+ <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
+ >v{{ appInfo.python_version }}</span
+ >
+ </div>
+ <div v-if="electronVersion" class="flex flex-col">
+ <span class="text-[10px] font-black text-blue-500 uppercase">Electron</span>
+ <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
+ >v{{ electronVersion }}</span
+ >
+ </div>
+ <div v-if="chromeVersion" class="flex flex-col">
+ <span class="text-[10px] font-black text-blue-500 uppercase">Chrome</span>
+ <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
+ >v{{ chromeVersion }}</span
+ >
+ </div>
+ <div v-if="nodeVersion" class="flex flex-col">
+ <span class="text-[10px] font-black text-blue-500 uppercase">Node</span>
+ <span class="text-sm font-medium text-gray-700 dark:text-zinc-300"
+ >v{{ nodeVersion }}</span
+ >
+ </div>
</div>
</div>
- </div>
- <!-- Python Dependencies -->
- <div
- v-if="appInfo.dependencies"
- class="mt-8 pt-6 border-t border-gray-100 dark:border-zinc-800"
- >
- <div
- class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
- >
- {{ $t("about.backend_dependencies") }}
- </div>
- <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
- <div v-for="(version, name) in appInfo.dependencies" :key="name" class="flex flex-col">
- <span class="text-[10px] font-black text-blue-500/70 uppercase">{{
- name.replace("_", " ")
- }}</span>
- <span class="text-xs font-medium text-gray-600 dark:text-zinc-400"
- >v{{ version }}</span
- >
+ <!-- Python Dependencies -->
+ <div v-if="appInfo.dependencies" class="pt-6 border-t border-gray-100 dark:border-zinc-800">
+ <div
+ class="text-xs font-bold text-gray-400 dark:text-zinc-500 uppercase tracking-wider mb-3"
+ >
+ {{ $t("about.backend_dependencies") }}
+ </div>
+ <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
+ <div v-for="(version, name) in appInfo.dependencies" :key="name" class="flex flex-col">
+ <span class="text-[10px] font-black text-blue-500/70 uppercase">{{
+ name.replace("_", " ")
+ }}</span>
+ <span class="text-xs font-medium text-gray-600 dark:text-zinc-400"
+ >v{{ version }}</span
+ >
+ </div>
</div>
</div>
</div>

diff --git a/meshchatx/src/frontend/components/call/CallOverlay.vue b/meshchatx/src/frontend/components/call/CallOverlay.vue
index 61cddda3..77d69173 100644
--- a/meshchatx/src/frontend/components/call/CallOverlay.vue
+++ b/meshchatx/src/frontend/components/call/CallOverlay.vue
@@ -69,7 +69,7 @@
:icon-name="activeCall.remote_icon ? activeCall.remote_icon.icon_name : ''"
:icon-foreground-colour="activeCall.remote_icon ? activeCall.remote_icon.foreground_colour : ''"
:icon-background-colour="activeCall.remote_icon ? activeCall.remote_icon.background_colour : ''"
- class="size-8"
+ icon-class="size-8"
/>
</div>
<div class="text-center w-full min-w-0">
@@ -246,7 +246,7 @@
:icon-name="activeCall.remote_icon ? activeCall.remote_icon.icon_name : ''"
:icon-foreground-colour="activeCall.remote_icon ? activeCall.remote_icon.foreground_colour : ''"
:icon-background-colour="activeCall.remote_icon ? activeCall.remote_icon.background_colour : ''"
- class="size-5 shrink-0"
+ icon-class="size-5 shrink-0"
/>
<div class="flex flex-col min-w-0">
<span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">

diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index cfd46c62..c68d134f 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -127,7 +127,7 @@
? (activeCall || lastCall).remote_icon.background_colour
: ''
"
- class="size-20"
+ icon-class="size-20"
/>
</div>
@@ -601,7 +601,7 @@
:icon-background-colour="
entry.remote_icon ? entry.remote_icon.background_colour : ''
"
- class="size-10"
+ icon-class="size-10"
/>
<div
class="absolute -bottom-1 -right-1 bg-white dark:bg-zinc-900 rounded-full p-0.5 shadow-sm border border-gray-100 dark:border-zinc-800 shrink-0 flex items-center justify-center size-5"

diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 80afb17d..a7a0aedd 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -35,7 +35,7 @@
:icon-background-colour="
selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.background_colour : ''
"
- class="size-10"
+ icon-class="size-10"
/>
</div>

diff --git a/meshchatx/src/frontend/components/profile/ProfileIconPage.vue b/meshchatx/src/frontend/components/profile/ProfileIconPage.vue
index fa6edd2d..e79bd9ba 100644
--- a/meshchatx/src/frontend/components/profile/ProfileIconPage.vue
+++ b/meshchatx/src/frontend/components/profile/ProfileIconPage.vue
@@ -4,7 +4,7 @@
<div class="max-w-4xl mx-auto p-4 space-y-6">
<!-- Header with Preview -->
<div
- class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
+ class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800"
>
<div class="p-6 border-b border-gray-200 dark:border-zinc-800">
<div class="flex items-center justify-between">
@@ -51,6 +51,7 @@
<div class="text-sm font-medium text-gray-700 dark:text-zinc-300">Preview</div>
<div class="p-8 bg-gray-50 dark:bg-zinc-800 rounded-2xl">
<LxmfUserIcon
+ :key="iconName + iconForegroundColour + iconBackgroundColour"
:icon-name="iconName"
:icon-foreground-colour="iconForegroundColour"
:icon-background-colour="iconBackgroundColour"
@@ -66,7 +67,7 @@
<!-- Color Selection -->
<div
- class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
+ class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Colors</h3>
@@ -252,14 +253,17 @@ export default {
},
},
watch: {
- config() {
- if (this.config) {
- this.iconName = this.config.lxmf_user_icon_name || null;
- this.iconForegroundColour = this.config.lxmf_user_icon_foreground_colour || "#6b7280";
- this.iconBackgroundColour = this.config.lxmf_user_icon_background_colour || "#e5e7eb";
+ config: {
+ handler() {
+ if (this.config) {
+ this.iconName = this.config.lxmf_user_icon_name || null;
+ this.iconForegroundColour = this.config.lxmf_user_icon_foreground_colour || "#6b7280";
+ this.iconBackgroundColour = this.config.lxmf_user_icon_background_colour || "#e5e7eb";
- this.saveOriginalValues();
- }
+ this.saveOriginalValues();
+ }
+ },
+ immediate: true,
},
iconForegroundColour() {
this.debouncedAutoSave();


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────